<?php
/**
 * ActiveRecord Item.
 *
 * @package    XDao Framework
 * @author     Cristian Buda <hello@phalien.com>
 * @author     Bogdan Stanescu <bstanescu@gmail.com>
 * @copyright  (c) 2011
 */
class XDao_Item
{
	// Row values
	public $properties;

	// Stores status of column since load / last save
	public $property_changes;

	// Table schema
	public $schema;

	// Value object reference (if scheme->vo is not specified, stdClass is returned)
	// public $vo;

	/**
	 * Overload methods to access external relations.
	 *
	 * @param   string  related object
	 * @param   array   optonal params
	 * @return  mixed
	 */
	public function __call($method, $params)
	{
		// Item must be initialized
		if (!($this->properties[$this->schema->primary])) return FALSE;
		
		$action = FALSE;
		
		// Check if we have a add_ method
		if ('add_' === substr($method, 0, 4))
		{
			$action = 'add';
			$method = substr($method, 4);
		}
		// Or a remove_ method
		elseif ('remove_' === substr($method, 0, 7))
		{
			$action = 'remove';
			$method = substr($method, 7);
		}
		// Otherwise must be a get method
		else
		{
			$action = 'get';
		}
		
		if (!isset($this->schema->relations[$method])) return FALSE;
		
		switch ($action)
		{
			case 'add':
			{
				$table = new $this->schema->relations[$method][XDb::REL_CLASS];
				
				// Determines join table
				if (isset($this->schema->relations[$method][XDb::REL_JOIN_TABLE]))
				{
					$join_table = $this->schema->relations[$method][XDb::REL_JOIN_TABLE];
				}
				else
				{
					// Compute default name as alpha2beta
					$arr = array($this->schema->table, $table->schema()->table);
					sort($arr);
					$join_table = implode('2', $arr);
				}

				// Name of current table's key column in the join table
				$own_key = isset($this->schema->relations[$method][XDb::REL_OWN_KEY]) 
					? $this->schema->relations[$method][XDb::REL_OWN_KEY] : $this->schema->columns[$this->schema->primary]->name;

				// Name of related table's key column in the join table
				$rel_key = isset($this->schema->relations[$method][XDb::REL_KEY]) 
					? $this->schema->relations[$method][XDb::REL_KEY] : $table->schema()->columns[$table->schema()->primary]->name;

				// Fetch IDs
				$ids = (isset($params[0])) ? $params[0] : FALSE;
				if (!is_array($ids)) $ids = array($ids);

				if (empty($params[0]))
				{
					$existing_ids = array();
				}
				else
				{
					// Fetch IDs of related items
					$sql = '
						SELECT ' . $rel_key . '
						FROM ' . $join_table . '
						WHERE 
							' . $own_key . ' = ' . $this->properties[$this->schema->primary] . '
							AND ' . $rel_key . ' IN (' . implode(', ', $ids) . ')
					';

					if (!$existing_ids = $this->schema->db->QueryColumn($sql)) $existing_ids = array();
				}
				
				if ($new_ids = array_diff($ids, $existing_ids))
				{
					foreach ($new_ids as $id)
					{
						$sql = '
							INSERT INTO ' . $join_table . '
							SET 
								' . $own_key . ' = ' . intval($this->properties[$this->schema->primary]) . ',
								' . $rel_key . ' = ' . intval($id) . '
						';
						$this->schema->db->Execute($sql);
					}
				}

				return TRUE;
			}

			case 'remove':
			{
				$table = new $this->schema->relations[$method][XDb::REL_CLASS];
				
				// Determins join table
				if (isset($this->schema->relations[$method][XDb::REL_JOIN_TABLE]))
				{
					$join_table = $this->schema->relations[$method][XDb::REL_JOIN_TABLE];
				}
				else
				{
					$arr = array($this->schema->table, $table->schema()->table);
					sort($arr);
					$join_table = implode('2', $arr);
				}

				// Name of current table's key column in the join table
				$own_key = isset($this->schema->relations[$method][XDb::REL_OWN_KEY]) 
					? $this->schema->relations[$method][XDb::REL_OWN_KEY] : $this->schema->columns[$this->schema->primary]->name;

				// Name of related table's key column in the join table
				$rel_key = isset($this->schema->relations[$method][XDb::REL_KEY]) 
					? $this->schema->relations[$method][XDb::REL_KEY] : $table->schema()->columns[$table->schema()->primary]->name;

				// Fetch IDs
				$ids = (isset($params[0])) ? $params[0] : FALSE;
				
				if ($ids)
				{
					if (!is_array($ids)) $ids = array($ids);

					// Fetch IDs of related items
					$sql = '
						SELECT ' . $rel_key . '
						FROM ' . $join_table . '
						WHERE 
							' . $own_key . ' = ' . $this->properties[$this->schema->primary] . '
							AND ' . $rel_key . ' IN (' . implode(', ', $ids) . ')
					';
					if (!$existing_ids = $this->schema->db->QueryColumn($sql)) $existing_ids = array();

					if ($delete_ids = array_intersect($existing_ids, $ids))
					{
						foreach ($delete_ids as $id)
						{
							$sql = '
								DELETE FROM ' . $join_table . '
								WHERE
									' . $own_key . ' = ' . intval($this->properties[$this->schema->primary]) . '
									AND ' . $rel_key . ' = ' . intval($id) . '
							';
							$this->schema->db->Execute($sql);
						}
					}
				}	
				else
				{
					$sql = '
						DELETE FROM ' . $join_table . '
						WHERE
							' . $own_key . ' = ' . intval($this->properties[$this->schema->primary]) . '
					';
					$this->schema->db->Execute($sql);
				}

				return TRUE;
			}

			case 'get':
			{				
				switch ($this->schema->relations[$method][0])
				{					
					case XDb::BELONGS_TO:
					{	
						// if ItemI18n
						$table = new $this->schema->relations[$method][XDb::REL_CLASS];
						if (isset($this->langId) && property_exists($table, 'langId'))
						{
							$table->setLangId($this->langId);
						}
						
						$foreign_key = isset($this->schema->relations[$method][XDb::REL_JOIN_TABLE]) 
							? $this->schema->relations[$method][XDb::REL_JOIN_TABLE] : $table->schema()->columns[$table->schema()->primary]->name;

						return $table->fetch($this->properties[$foreign_key]);
					}

					case XDb::HAS_ONE:
					{
						// if ItemI18n
						$table = new $this->schema->relations[$method][XDb::REL_CLASS];
						if (isset($this->langId) && property_exists($table, 'langId'))
						{
							$table->setLangId($this->langId);
						}

						$foreign_key = isset($this->schema->relations[$method][XDb::REL_JOIN_TABLE]) 
							? $this->schema->relations[$method][XDb::REL_JOIN_TABLE] : $table->schema()->columns[$table->schema()->primary]->name;

						return $table->fetch($this->properties[$table->schema()->primary]);
					}

					case XDb::HAS_MANY:
					{
						// if ItemI18n
						$table = new $this->schema->relations[$method][XDb::REL_CLASS];
						if (isset($this->langId) && property_exists($table, 'langId'))
						{
							$table->setLangId($this->langId);
						}
						
						$foreign_key = isset($this->schema->relations[$method][XDb::REL_JOIN_TABLE]) 
							? $this->schema->relations[$method][XDb::REL_JOIN_TABLE] : $this->schema->columns[$this->schema->primary]->name;

						return $table->find()->where($foreign_key, '=', $this->properties[$this->schema->primary]);
					}

					case XDb::MANY_TO_MANY:
					{
						// if ItemI18n
						$table = new $this->schema->relations[$method][XDb::REL_CLASS];
						if (isset($this->langId) && property_exists($table, 'langId'))
						{
							$table->setLangId($this->langId);
						}
						
						// Determins join table
						if (isset($this->schema->relations[$method][XDb::REL_JOIN_TABLE]))
						{
							$join_table = $this->schema->relations[$method][XDb::REL_JOIN_TABLE];
						}
						else
						{
							$arr = array($this->schema->table, $table->schema()->table);
							sort($arr);
							$join_table = implode('2', $arr);
						}
						
						// Name of current table's key column in the join table
						$own_key = isset($this->schema->relations[$method][XDb::REL_OWN_KEY]) 
							? $this->schema->relations[$method][XDb::REL_OWN_KEY] : $this->schema->columns[$this->schema->primary]->name;

						// Name of related table's key column in the join table
						$rel_key = isset($this->schema->relations[$method][XDb::REL_KEY]) 
							? $this->schema->relations[$method][XDb::REL_KEY] : $table->schema()->columns[$table->schema()->primary]->name;

						// Fetch IDs of related items
						$sql = '
							SELECT ' . $rel_key . '
							FROM ' . $join_table . '
							WHERE ' . $own_key . ' = ' . $this->properties[$this->schema->primary] . '
						';
						$ids = $this->schema->db->QueryColumn($sql);
						if (!$ids) $ids = array(-1); // @todo Find something more inteligent here

						return $table->find()->where($table->schema()->primary, 'IN', $ids);
					}
				}

				return $this->relations[$ident];
			}
		}
	}

	/**
	 * Stores table schema.
	 *
	 * @return void
	 */
	public function __construct($schema)
	{
		$this->schema = $schema;
		foreach (array_keys($this->schema->columns) as $column_name)
		{
			$this->properties[$column_name] = NULL;
			$this->property_changes[$column_name] = 0;
		}
	}

	/**
	 * Returns the value for a column, or the value object itself.
	 *
	 * @param	string	column name
	 * @return 	mixed
	 */
	public function __get($column)
	{
		// value object requested
		if ($column == 'vo')
		{
			if (isset($this->schema->vo))
			{
				$vo = new $this->schema->vo;
			}
			else
			{
				$vo = new stdClass;			
			}
			// Populate values
			foreach ($this->properties as $key => $value)
			{
				// // populate value object
				// if (isset($this->schema->vo) && property_exists($vo, $key))
				{				
					$vo->$key = $value;
				}
			}
			return $vo;
		}
		
		if (!array_key_exists($column, $this->schema->columns)) XDao_Model::trigger_error(XDao_Model::ERR_COLUMN_NON_EXISTING, $column);

		return $this->properties[$column];
	}
	
	/**
	 * Stores value for a column.
	 *
	 * @param	string	column name
	 * @param	mixed	column value
	 * @return 	void
	 */
	public function __set($column, $value)
	{
		// value object requested
		if ($column == 'vo' && is_object($value))
		{
			// Populate values
			foreach ($this->schema->columns as $col => $val)
			{
				if (property_exists($value, $col))
				{
					$this->properties[$col] = $value->$col;
					$this->property_changes[$col] = 1;
				}
			}
		}
		else
		{
			if (!array_key_exists($column, $this->schema->columns)) XDao_Model::trigger_error(XDao_Model::ERR_COLUMN_NON_EXISTING, $column);
	
			$this->properties[$column] = $value;
			$this->property_changes[$column] = 1;
		}
	}

	/**
	 * Deletes a row.
	 *
	 * @return boolean
	 */
	public function delete()
	{
		// Primary field must be set
		if (!$this->properties[$this->schema->primary]) return FALSE;

		$sql = '
			DELETE FROM ' . $this->schema->table . '
			WHERE ' . $this->schema->columns[$this->schema->primary]->name . ' = ' . 
				$this->schema->db->Escape($this->properties[$this->schema->primary], $this->schema->columns[$this->schema->primary]->type) . '
		';

		// Execute query
		if (!$this->schema->db->Execute($sql)) return FALSE;
		
		// Clears current instance
		$this->reset();
		
		return TRUE;
	}
	
	/**
	 * Loads an item in current object.
	 *
	 * @param   array    associative array with column values
	 * @return  boolean
	 */
	public function load($id)
	{
		$sql = '
			SELECT * 
			FROM ' . $this->schema->table . '
			WHERE ' . $this->schema->columns[$this->schema->primary]->name . ' = ' . 
				$this->schema->db->Escape($id, $this->schema->columns[$this->schema->primary]->type) . '
		';

		// Execute query
		if ($row = $this->schema->db->QueryRow($sql))
		{
			$this->populate($row);
			return TRUE;
		}
		
		return FALSE;
	}

	/**
	 * Populates current object with a row from DB.
	 * Objects are populated by XDao_Model methods so that we can use one query for retrieving multiple objects.
	 *
	 * @param   array   associative array with column values
	 * @return  void
	 */
	public function populate($row)
	{
		// Populate values
		foreach ($this->schema->columns as $nice_name => $column)
		{
			$this->property_changes[$nice_name] = 0;
			$this->properties[$nice_name] = $row[$column->name];
		}
	}

	/**
	 * Clears current instance.
	 *
	 * @return void
	 */
	private function reset()
	{
		foreach (array_keys($this->schema->columns) as $column_name)
		{
			$this->property_changes[$column_name] = 0;
			$this->properties[$column_name] = NULL;
		}
	}

	/**
	 * Insert / update current values.
	 *
	 * @example $instance = new Model; $object = $instance->fetch(2); $object->save();
	 *
	 * @return boolean
	 */
	public function save()
	{
		// Check if we have changes
		if (0 == array_sum($this->property_changes)) return TRUE;
		
		// Determin whether it's an insert or an update
		$sql = ($this->properties[$this->schema->primary] ? 'UPDATE ' : 'INSERT INTO ') . $this->schema->table . ' SET ';

		// Build query
		$sep = '';
		foreach (array_keys($this->schema->columns) as $column_name)
		{
			if ($this->property_changes[$column_name])
			{
				$sql .= $sep . $this->schema->columns[$column_name]->name . ' = ' . 
					$this->schema->db->Escape($this->properties[$column_name], $this->schema->columns[$column_name]->type);
				$sep = ', ';
			}
		}
		if ($this->properties[$this->schema->primary])
		{
			$sql .= ' WHERE ' . $this->schema->columns[$this->schema->primary]->name . ' = ' . 
				$this->schema->db->Escape($this->properties[$this->schema->primary], $this->schema->columns[$this->schema->primary]->type);
		}

		// Execute query
		if (!$this->schema->db->Execute($sql)) return FALSE;
		
		// If it's a new row, fetch the last inserted ID
		if (!$this->properties[$this->schema->primary])
		{
			$this->properties[$this->schema->primary] = $this->schema->db->GetInsertId();
		}

		// Reset column changes flags
		foreach (array_keys($this->schema->columns) as $column_name)
		{
			$this->property_changes[$column_name] = 0;
		}
		
		return TRUE;
	}

	/**
	 * Returns an associative array with item's properties.
	 *
	 * @return array
	 */
	public function to_array()
	{
		return $this->properties;
	}
	
	
	/**
	 * Populate properties from associative array
	 *
	 * @param array $array 
	 * @return void
	 * @author Bogdan Stanescu
	 */
	public function from_array($array)
	{
		if (!empty($array) && is_array($array))
		{
			foreach($array as $key => $value)
			{
				
				if (!array_key_exists($key, $this->schema->columns))
				{
					// XDao_Model::trigger_error(XDao_Model::ERR_COLUMN_NON_EXISTING, $column);
				}
				else
				{
					$this->properties[$key] = $value;
					$this->property_changes[$key] = 1;
				}
				
			}
		}
		return $this;
	}	
}

?>